ποΈGitΠ―ΡΠ°ποΈ
Node / meshtastic / Meshtastic-Android / files / core / network / src / commonMain / kotlin / org / meshtastic / core / network / radio / BleRadioTransport.kt
Displaying Raw β’ Download
core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt renovate/net.java.dev.jna-jna-5.x (5a8f4966) Text, 23.04 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72package T7ee787org.meshtastic.core.network.radio
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.CoroutineExceptionHandler
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.NonCancellable
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.cancel
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.flow.catch
Tff7b72import T7ee787kotlinx.coroutines.flow.filterIsInstance
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.flow.launchIn
Tff7b72import T7ee787kotlinx.coroutines.flow.onEach
Tff7b72import T7ee787kotlinx.coroutines.job
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.sync.Mutex
Tff7b72import T7ee787kotlinx.coroutines.sync.withLock
Tff7b72import T7ee787kotlinx.coroutines.withContext
Tff7b72import T7ee787kotlinx.coroutines.withTimeoutOrNull
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnection
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionFactory
Tff7b72import T7ee787org.meshtastic.core.ble.BleConnectionState
Tff7b72import T7ee787org.meshtastic.core.ble.BleDevice
Tff7b72import T7ee787org.meshtastic.core.ble.BleScanner
Tff7b72import T7ee787org.meshtastic.core.ble.BleWriteType
Tff7b72import T7ee787org.meshtastic.core.ble.BluetoothRepository
Tff7b72import T7ee787org.meshtastic.core.ble.DisconnectReason
Tff7b72import T7ee787org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
Tff7b72import T7ee787org.meshtastic.core.ble.MeshtasticRadioProfile
Tff7b72import T7ee787org.meshtastic.core.ble.classifyBleException
Tff7b72import T7ee787org.meshtastic.core.ble.retryBleOperation
Tff7b72import T7ee787org.meshtastic.core.ble.toMeshtasticRadioProfile
Tff7b72import T7ee787org.meshtastic.core.common.util.nowMillis
Tff7b72import T7ee787org.meshtastic.core.model.RadioNotConnectedException
Tff7b72import T7ee787org.meshtastic.core.network.transport.HeartbeatSender
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransport
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransportCallback
Tff7b72import T7ee787kotlin.concurrent.Volatile
Tff7b72import T7ee787kotlin.time.Duration.Companion.milliseconds
Tff7b72import T7ee787kotlin.time.Duration.Companion.seconds
Tff7b72private Tff7b72const Tff7b72val Te6edf3SCAN_RETRY_COUNT Tff7b72= T79c0ff3
Tff7b72private Tff7b72val Te6edf3SCAN_RETRY_DELAY Tff7b72= T79c0ff1.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3CONNECTION_TIMEOUT Tff7b72= T79c0ff1T79c0ff5.Te6edf3seconds
T8b949e/**
* Delay after writing a heartbeat before re-polling FROMRADIO.
*
* The ESP32 firmware processes TORADIO writes asynchronously (NimBLE callback β FreeRTOS main task queue β
* `handleToRadio()` β `heartbeatReceived = true`). The immediate drain trigger in
* [KableMeshtasticRadioProfile.sendToRadio] fires before this completes, so the `queueStatus` response is not yet
* available. 200 ms is well above observed ESP32 task scheduling latency (~10β50 ms) while remaining imperceptible to
* the user.
*/
Tff7b72private Tff7b72val Te6edf3HEARTBEAT_DRAIN_DELAY Tff7b72= T79c0ff2T79c0ff0T79c0ff0.Te6edf3milliseconds
Tff7b72private Tff7b72val Te6edf3SCAN_TIMEOUT Tff7b72= T79c0ff5.Te6edf3seconds
Tff7b72private Tff7b72val Te6edf3GATT_CLEANUP_TIMEOUT Tff7b72= T79c0ff5.Te6edf3seconds
T8b949e/**
* Delay after onConnect before downgrading BLE connection priority to Balanced.
*
* The initial config drain (fromRadio burst) typically completes within 2β5 seconds on most devices, but slower radios
* (ESP32 with large node databases, many channels, or dense position history) can take significantly longer. 30 seconds
* provides generous margin while still ensuring we don't sustain the 7.5 ms connection interval indefinitely, which
* significantly increases battery draw on both the phone and the radio.
*/
Tff7b72private Tff7b72val Te6edf3PRIORITY_DOWNGRADE_DELAY Tff7b72= T79c0ff3T79c0ff0.Te6edf3seconds
T8b949e/**
* A [RadioTransport] implementation for BLE devices using the common BLE abstractions (which are powered by Kable).
*
* This class handles the high-level connection lifecycle for Meshtastic radios over BLE, including:
* - Bonding and discovery.
* - Automatic reconnection logic.
* - MTU and connection parameter monitoring.
* - Routing raw byte packets between the radio and [RadioTransportCallback].
*
* @param scope The coroutine scope to use for launching coroutines.
* @param scanner The BLE scanner.
* @param bluetoothRepository The Bluetooth repository.
* @param connectionFactory The BLE connection factory.
* @param callback The [RadioTransportCallback] to use for handling radio events.
* @param address The BLE address of the device to connect to.
*/
Tff7b72class T56d364BleRadioTransportTb4b4b4(
Tff7b72private Tff7b72val Te6edf3scopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4,
Tff7b72private Tff7b72val Te6edf3scannerTb4b4b4: Te6edf3BleScannerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3bluetoothRepositoryTb4b4b4: Te6edf3BluetoothRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3connectionFactoryTb4b4b4: Te6edf3BleConnectionFactoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3callbackTb4b4b4: Te6edf3RadioTransportCallbackTb4b4b4,
Tff7b72internal Tff7b72val Te6edf3addressTb4b4b4: Tffa657StringTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3RadioTransport Tb4b4b4{
T8b949e// Detached cleanup scope for last-ditch GATT teardown from the exception handler.
T8b949e// Must NOT be a child of `scope`: when an uncaught exception fires in connectionScope,
T8b949e// upper layers often tear down `scope` immediately. Launching cleanup on `scope` then
T8b949e// races the cancellation and may never start, leaking BluetoothGatt (status 133 on
T8b949e// the next reconnect). This scope is cancelled in close() once our own disconnect
T8b949e// has completed and the safety net is no longer needed.
Tff7b72private Tff7b72val Te6edf3cleanupScopeTb4b4b4: Te6edf3CoroutineScope Tff7b72= Te6edf3CoroutineScopeTb4b4b4(Te6edf3SupervisorJobTb4b4b4(Tb4b4b4) Tff7b72+ Te6edf3scopeTb4b4b4.Te6edf3coroutineContextTb4b4b4.Te6edf3minusKeyTb4b4b4(Te6edf3JobTb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3exceptionHandler Tff7b72= Te6edf3CoroutineExceptionHandler Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3throwable Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3throwableTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Uncaught exception in connectionScopeTa5d6ff" Tb4b4b4}
Te6edf3cleanupScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to disconnect in exception handlerTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72val Tb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3msgTb4b4b4) Tff7b72= Te6edf3throwableTb4b4b4.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)
Te6edf3callbackTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3connectionScopeTb4b4b4: Te6edf3CoroutineScope Tff7b72=
Te6edf3CoroutineScopeTb4b4b4(Te6edf3scopeTb4b4b4.Te6edf3coroutineContext Tff7b72+ Te6edf3SupervisorJobTb4b4b4(Te6edf3scopeTb4b4b4.Te6edf3coroutineContextTb4b4b4.Te6edf3jobTb4b4b4) Tff7b72+ Te6edf3exceptionHandlerTb4b4b4)
Tff7b72private Tff7b72val Te6edf3bleConnectionTb4b4b4: Te6edf3BleConnection Tff7b72= Te6edf3connectionFactoryTb4b4b4.Te6edf3createTb4b4b4(Te6edf3connectionScopeTb4b4b4, Te6edf3addressTb4b4b4)
Tff7b72private Tff7b72val Te6edf3writeMutexTb4b4b4: Te6edf3Mutex Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3connectionStartTimeTb4b4b4: Tffa657Long Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3packetsReceivedTb4b4b4: Tffa657Int Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3packetsSentTb4b4b4: Tffa657Int Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3bytesReceivedTb4b4b4: Tffa657Long Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3bytesSentTb4b4b4: Tffa657Long Tff7b72= T79c0ff0
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3isFullyConnected Tff7b72= Tff7b72false
Tff7b72private Tff7b72var Te6edf3connectionJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null
T8b949e// Never give up while the user has this device selected. Higher layers (SharedRadioInterfaceService)
T8b949e// own the explicit-disconnect lifecycle and will close() us when the user picks a different device or
T8b949e// toggles the connection off; until then, retry forever with the policy's exponential-backoff cap (60 s).
Tff7b72private Tff7b72val Te6edf3reconnectPolicy Tff7b72= Te6edf3BleReconnectPolicyTb4b4b4(Te6edf3maxFailures Tff7b72= Tffa657IntTb4b4b4.Te6edf3MAX_VALUETb4b4b4)
Tff7b72private Tff7b72val Te6edf3heartbeatSender Tff7b72=
Te6edf3HeartbeatSenderTb4b4b4(
Te6edf3sendToRadio Tff7b72= Tff7b72::Te6edf3handleSendToRadioTb4b4b4,
Te6edf3afterHeartbeat Tff7b72= Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3HEARTBEAT_DRAIN_DELAYTb4b4b4)
Te6edf3radioServiceTff7b72?.Te6edf3requestDrainTb4b4b4(Tb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3logTag Tff7b72= Te6edf3addressTb4b4b4,
Tb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffstartTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3connectTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e// --- Connection & Discovery Logic ---
T8b949e/** Robustly finds the device. First checks bonded devices, then performs a short scan if not found. */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8fffindDeviceTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BleDevice Tb4b4b4{
Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3stateTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3bondedDevices
Tb4b4b4.Te6edf3firstOrNull Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3addressTb4b4b4.Te6edf3equalsTb4b4b4(Te6edf3addressTb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4) Tb4b4b4}
Tff7b72?.Te6edf3let Tb4b4b4{
Tff7b72return Tffa657it
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Device not found in bonded list, scanningTa5d6ff" Tb4b4b4}
Te6edf3repeatTb4b4b4(Te6edf3SCAN_RETRY_COUNTTb4b4b4) Tb4b4b4{ Te6edf3attempt Tff7b72-Tff7b72>
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3d Tff7b72=
Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3SCAN_TIMEOUTTb4b4b4) Tb4b4b4{
T8b949e// Pass both service UUID and address so the scanner can apply the most
T8b949e// efficient platform filter. Android uses address (OS-level HW filter),
T8b949e// while CoreBluetooth (macOS) needs the service UUID because it caches
T8b949e// peripheral identifiers and may not re-report by address alone.
Te6edf3scannerTb4b4b4.Te6edf3scanTb4b4b4(Te6edf3timeout Tff7b72= Te6edf3SCAN_TIMEOUTTb4b4b4, Te6edf3serviceUuid Tff7b72= Te6edf3SERVICE_UUIDTb4b4b4, Te6edf3address Tff7b72= Te6edf3addressTb4b4b4)Tb4b4b4.Te6edf3first Tb4b4b4{
Tffa657itTb4b4b4.Te6edf3addressTb4b4b4.Te6edf3equalsTb4b4b4(Te6edf3addressTb4b4b4, Te6edf3ignoreCase Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3d Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tff7b72return Te6edf3d
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3vTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Scan attempt failed or timed outTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3attempt Tff7b72< Te6edf3SCAN_RETRY_COUNT Tff7b72- T79c0ff1Tb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3SCAN_RETRY_DELAYTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72throw Te6edf3RadioNotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffDevice not found at address Tffd700$Te6edf3addressTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffconnectTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3connectionJob Tff7b72=
Te6edf3connectionScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3reconnectPolicyTb4b4b4.Te6edf3executeTb4b4b4(
Te6edf3attempt Tff7b72= Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3attemptConnectionTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3failureTime Tff7b72= Tb4b4b4(Te6edf3nowMillis Tff7b72- Te6edf3connectionStartTimeTb4b4b4)Tb4b4b4.Te6edf3milliseconds
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to connect after Tffd700$Te6edf3failureTimeTa5d6ff" Tb4b4b4}
Te6edf3BleReconnectPolicyTb4b4b4.Te6edf3OutcomeTb4b4b4.Te6edf3FailedTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3onTransientDisconnect Tff7b72= Tb4b4b4{ Te6edf3error Tff7b72-Tff7b72>
Tff7b72val Te6edf3msg Tff7b72= Te6edf3errorTff7b72?.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3second Tff7b72?: Ta5d6ff"Ta5d6ffDevice unreachableTa5d6ff"
Te6edf3callbackTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72falseTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tb4b4b4}Tb4b4b4,
Te6edf3onPermanentDisconnect Tff7b72= Tb4b4b4{ Te6edf3error Tff7b72-Tff7b72>
Tff7b72val Te6edf3msg Tff7b72= Te6edf3errorTff7b72?.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3second Tff7b72?: Ta5d6ff"Ta5d6ffDevice unreachableTa5d6ff"
Te6edf3callbackTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72trueTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Performs a single BLE connect-and-wait cycle.
*
* Finds the device, bonds if needed, connects, discovers services, and waits for disconnect. Returns a
* [BleReconnectPolicy.Outcome] describing how the connection ended.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffCyclomaticComplexMethodTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffattemptConnectionTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3BleReconnectPolicyTb4b4b4.Te6edf3Outcome Tb4b4b4{
Te6edf3connectionStartTime Tff7b72= Te6edf3nowMillis
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE connection attempt startedTa5d6ff" Tb4b4b4}
Tff7b72val Te6edf3device Tff7b72= Te6edf3findDeviceTb4b4b4(Tb4b4b4)
T8b949e// Bond before connecting: firmware may require an encrypted link,
T8b949e// and without a bond Android fails with status 5 or 133.
T8b949e// No-op on Desktop/JVM where the OS handles pairing automatically.
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3isBondedTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Device not bonded, initiating bondingTa5d6ff" Tb4b4b4}
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72try Tb4b4b4{
Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3bondTb4b4b4(Te6edf3deviceTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Bonding successfulTa5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Bonding failed, attempting connection anywayTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72val Te6edf3state Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3connectAndAwaitTb4b4b4(Te6edf3deviceTb4b4b4, Te6edf3CONNECTION_TIMEOUTTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3state Tff7b72!is Te6edf3BleConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3RadioNotConnectedExceptionTb4b4b4(Ta5d6ff"Ta5d6ffFailed to connect to device at address Tffd700$Te6edf3addressTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tff7b72val Te6edf3gattConnectedAt Tff7b72= Te6edf3nowMillis
Te6edf3isFullyConnected Tff7b72= Tff7b72true
Te6edf3onConnectedTb4b4b4(Tb4b4b4)
Te6edf3discoverServicesAndSetupCharacteristicsTb4b4b4(Tb4b4b4)
T8b949e// Wait for the StateFlow to actually reflect Connected before watching for the next
T8b949e// Disconnected. connectAndAwait returns synchronously based on the underlying Kable
T8b949e// peripheral state, but our _connectionState observer runs on a separate coroutine and
T8b949e// may lag. Without this gate the next .first { Disconnected } below could match the
T8b949e// *previous* cycle's stale Disconnected value and fire immediately, breaking reconnect.
Te6edf3bleConnectionTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3first Tb4b4b4{ Tffa657it Tff7b72is Te6edf3BleConnectionStateTb4b4b4.Te6edf3Connected Tb4b4b4}
T8b949e// Suspend until the next Disconnected emission. We deliberately do NOT wrap this in a
T8b949e// coroutineScope { launchIn(...); first(...) } pattern: launching a hot StateFlow
T8b949e// collector inside coroutineScope hangs the scope after .first returns (the launched
T8b949e// collector never completes naturally, and coroutineScope waits for all children).
Tff7b72val Te6edf3disconnectedState Tff7b72=
Te6edf3bleConnectionTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3filterIsInstanceTff7b72<Te6edf3BleConnectionStateTb4b4b4.Te6edf3DisconnectedTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3disconnectReason Tff7b72= Te6edf3disconnectedStateTb4b4b4.Te6edf3reason
Tff7b72if Tb4b4b4(Te6edf3isFullyConnectedTb4b4b4) Tb4b4b4{
Te6edf3isFullyConnected Tff7b72= Tff7b72false
Te6edf3onDisconnectedTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE connection dropped (reason: Tffd700$Te6edf3disconnectReasonTa5d6ff), preparing to reconnectTa5d6ff" Tb4b4b4}
Tff7b72val Te6edf3wasIntentional Tff7b72= Te6edf3disconnectReason Tff7b72is Te6edf3DisconnectReasonTb4b4b4.Te6edf3LocalDisconnect
Tff7b72val Te6edf3connectionUptime Tff7b72= Tb4b4b4(Te6edf3nowMillis Tff7b72- Te6edf3gattConnectedAtTb4b4b4)Tb4b4b4.Te6edf3milliseconds
Tff7b72val Te6edf3wasStable Tff7b72= Te6edf3connectionUptime Tff7b72>Tff7b72= Te6edf3reconnectPolicyTb4b4b4.Te6edf3minStableConnection
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3wasStable Tff7b72&Tff7b72& Tff7b72!Te6edf3wasIntentionalTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Connection lasted only Tffd700$Te6edf3connectionUptimeTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(< Tffd700${Te6edf3reconnectPolicyTb4b4b4.Te6edf3minStableConnectionTffd700}Ta5d6ff) β treating as unstableTa5d6ff"
Tb4b4b4}
Tb4b4b4}
Tff7b72return Te6edf3BleReconnectPolicyTb4b4b4.Te6edf3OutcomeTb4b4b4.Te6edf3DisconnectedTb4b4b4(Te6edf3wasStable Tff7b72= Te6edf3wasStableTb4b4b4, Te6edf3wasIntentional Tff7b72= Te6edf3wasIntentionalTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffonConnectedTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3deviceFlowTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3device Tff7b72-Tff7b72>
Tff7b72val Te6edf3rssi Tff7b72= Te6edf3retryBleOperationTb4b4b4(Te6edf3tag Tff7b72= Te6edf3addressTb4b4b4) Tb4b4b4{ Te6edf3deviceTb4b4b4.Te6edf3readRssiTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Connection confirmed. Initial RSSI: Tffd700$Te6edf3rssiTa5d6ff dBmTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to read initial connection RSSITa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffonDisconnectedTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3radioService Tff7b72= Tff7b72null
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE disconnected - Tffd700${Te6edf3formatSessionStatsTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4}
T8b949e// Signal immediately so the UI reflects the disconnect while reconnect continues.
Te6edf3callbackTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72falseTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffdiscoverServicesAndSetupCharacteristicsTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3profileTb4b4b4(Te6edf3serviceUuid Tff7b72= Te6edf3SERVICE_UUIDTb4b4b4) Tb4b4b4{ Te6edf3service Tff7b72-Tff7b72>
Tff7b72val Te6edf3radioService Tff7b72= Te6edf3serviceTb4b4b4.Te6edf3toMeshtasticRadioProfileTb4b4b4(Tb4b4b4)
Te6edf3radioServiceTb4b4b4.Te6edf3fromRadio
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3packet Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Received packet fromRadio (Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes)Ta5d6ff" Tb4b4b4}
Te6edf3dispatchPacketTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Error in fromRadio flowTa5d6ff" Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3radioServiceTb4b4b4.Te6edf3logRadio
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3packet Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Received packet logRadio (Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes)Ta5d6ff" Tb4b4b4}
Te6edf3dispatchPacketTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72>
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Error in logRadio flowTa5d6ff" Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Tff7b72thisTb4b4b4)
Tff7b72thisTf0883e@BleRadioTransport.radioService Tff7b72= Te6edf3radioService
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Profile service active and characteristics subscribedTa5d6ff" Tb4b4b4}
T8b949e// Wait for FROMNUM CCCD write before triggering the Meshtastic handshake.
Te6edf3radioServiceTb4b4b4.Te6edf3awaitSubscriptionReadyTb4b4b4(Tb4b4b4)
T8b949e// Log negotiated MTU for diagnostics
Tff7b72val Te6edf3maxLen Tff7b72= Te6edf3bleConnectionTb4b4b4.Te6edf3maximumWriteValueLengthTb4b4b4(Te6edf3BleWriteTypeTb4b4b4.Te6edf3WITHOUT_RESPONSETb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] BLE Radio Session Ready. Max write length (WITHOUT_RESPONSE): Tffd700$Te6edf3maxLenTa5d6ff bytesTa5d6ff" Tb4b4b4}
Te6edf3requestHighPriorityAndScheduleDowngradeTb4b4b4(Tb4b4b4)
Tff7b72thisTf0883e@BleRadioTransport.callback.onConnectTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
T8b949e// Scope was cancelled externally β still ensure GATT cleanup runs so we don't
T8b949e// leak a BluetoothGatt handle and trigger GATT status 133 on the next attempt.
Te6edf3withContextTb4b4b4(Te6edf3NonCancellableTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3ignoredTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3ignoredTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] disconnect() failed during cancellation cleanupTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72throw Te6edf3e
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Profile service discovery or operation failedTa5d6ff" Tb4b4b4}
T8b949e// Disconnect to let the outer reconnect loop see a clean Disconnected state.
T8b949e// Do NOT call handleFailure here β the reconnect loop owns failure counting.
Te6edf3withContextTb4b4b4(Te6edf3NonCancellableTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3ignoredTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3ignoredTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] disconnect() failed after profile errorTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/**
* Requests high BLE connection priority for the initial config burst, then schedules a downgrade to balanced
* priority after [PRIORITY_DOWNGRADE_DELAY] to conserve battery.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Te6edf3CoroutineScopeTb4b4b4.Td2a8ffrequestHighPriorityAndScheduleDowngradeTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3bleConnectionTb4b4b4.Te6edf3requestHighConnectionPriorityTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Requested high BLE connection priorityTa5d6ff" Tb4b4b4}
T8b949e// Wait for the connection parameter update before starting heavy traffic.
Te6edf3delayTb4b4b4(T79c0ff1.Te6edf3secondsTb4b4b4)
Tb4b4b4}
Te6edf3launch Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3PRIORITY_DOWNGRADE_DELAYTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3bleConnectionTb4b4b4.Te6edf3requestBalancedConnectionPriorityTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Downgraded to balanced BLE connection priorityTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3radioServiceTb4b4b4: Te6edf3MeshtasticRadioProfile? Tff7b72= Tff7b72null
T8b949e// --- RadioTransport Implementation ---
T8b949e/**
* Sends a packet to the radio with retry support.
*
* @param p The packet to send.
*/
Tff7b72override Tff7b72fun Td2a8ffhandleSendToRadioTb4b4b4(Te6edf3pTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3currentService Tff7b72= Te6edf3radioService
Tff7b72if Tb4b4b4(Te6edf3currentService Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3connectionScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3writeMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3retryBleOperationTb4b4b4(Te6edf3tag Tff7b72= Te6edf3addressTb4b4b4) Tb4b4b4{ Te6edf3currentServiceTb4b4b4.Te6edf3sendToRadioTb4b4b4(Te6edf3pTb4b4b4) Tb4b4b4}
Te6edf3packetsSentTff7b72+Tff7b72+
Te6edf3bytesSent Tff7b72+Tff7b72= Te6edf3pTb4b4b4.Te6edf3size
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Wrote packet #Tffd700$Te6edf3packetsSentTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffto toRadio (Tffd700${Te6edf3pTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes, total TX: Tffd700$Te6edf3bytesSentTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to write packet to toRadioCharacteristic after Ta5d6ff" Tff7b72+
Ta5d6ff"Tffd700$Te6edf3packetsSentTa5d6ff successful writesTa5d6ff"
Tb4b4b4}
Te6edf3handleFailureTb4b4b4(Te6edf3eTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] toRadio characteristic unavailable, can't send dataTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffkeepAliveTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Delegate to HeartbeatSender which sends a ToRadio heartbeat with a unique nonce
T8b949e// so the firmware resets its power-saving idle timer. After sending, it schedules
T8b949e// a delayed re-drain to pick up the queueStatus response.
Te6edf3connectionScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3heartbeatSenderTb4b4b4.Te6edf3sendHeartbeatTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}
T8b949e/** Closes the connection to the device. */
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffcloseTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Disconnecting. Tffd700${Te6edf3formatSessionStatsTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff" Tb4b4b4}
Te6edf3connectionScopeTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
T8b949e// GATT cleanup must run under NonCancellable so a cancelled caller cannot skip it,
T8b949e// which would leak BluetoothGatt and trigger status 133 on the next reconnect.
T8b949e// Using withContext (not runBlocking) keeps the caller's thread free β this is
T8b949e// critical when close() is invoked from the main thread during process shutdown.
Te6edf3withContextTb4b4b4(Te6edf3NonCancellableTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3GATT_CLEANUP_TIMEOUTTb4b4b4) Tb4b4b4{ Te6edf3bleConnectionTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Failed to disconnect in close()Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e// Our own disconnect succeeded β the exception-handler safety net is no longer
T8b949e// needed. Cancel the detached cleanup scope so it doesn't outlive us in tests
T8b949e// or process lifetime.
Te6edf3cleanupScopeTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffdispatchPacketTb4b4b4(Te6edf3packetTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
Te6edf3packetsReceivedTff7b72+Tff7b72+
Te6edf3bytesReceived Tff7b72+Tff7b72= Te6edf3packetTb4b4b4.Te6edf3size
Te6edf3LoggerTb4b4b4.Te6edf3v Tb4b4b4{
Ta5d6ff"Ta5d6ff[Tffd700$Te6edf3addressTa5d6ff] Dispatching packet #Tffd700$Te6edf3packetsReceivedTa5d6ff Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(Tffd700${Te6edf3packetTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytes, total RX: Tffd700$Te6edf3bytesReceivedTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
Te6edf3callbackTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3packetTb4b4b4)
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffhandleFailureTb4b4b4(Te6edf3throwableTb4b4b4: Te6edf3ThrowableTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3msgTb4b4b4) Tff7b72= Te6edf3throwableTb4b4b4.Te6edf3toDisconnectReasonTb4b4b4(Tb4b4b4)
Te6edf3callbackTb4b4b4.Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanentTb4b4b4, Te6edf3errorMessage Tff7b72= Te6edf3msgTb4b4b4)
Tb4b4b4}
T8b949e/** Formats a one-line session statistics summary for logging. */
Tff7b72private Tff7b72fun Td2a8ffformatSessionStatsTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657String Tb4b4b4{
Tff7b72val Te6edf3uptime Tff7b72= Tff7b72if Tb4b4b4(Te6edf3connectionStartTime Tff7b72> T79c0ff0Tb4b4b4) Te6edf3nowMillis Tff7b72- Te6edf3connectionStartTime Tff7b72else T79c0ff0
Tff7b72return Ta5d6ff"Ta5d6ffUptime: Tffd700${Te6edf3uptimeTffd700}Ta5d6ffms, Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffPackets RX: Tffd700$Te6edf3packetsReceivedTa5d6ff (Tffd700$Te6edf3bytesReceivedTa5d6ff bytes), Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffPackets TX: Tffd700$Te6edf3packetsSentTa5d6ff (Tffd700$Te6edf3bytesSentTa5d6ff bytes)Ta5d6ff"
Tb4b4b4}
Tff7b72private Tff7b72fun Te6edf3ThrowableTb4b4b4.Td2a8fftoDisconnectReasonTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3PairTff7b72<Tffa657BooleanTb4b4b4, Tffa657StringTff7b72> Tb4b4b4{
Te6edf3classifyBleExceptionTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{
Tff7b72return Tffa657itTb4b4b4.Te6edf3isPermanent Te6edf3to Tffa657itTb4b4b4.Te6edf3message
Tb4b4b4}
Tff7b72val Te6edf3msg Tff7b72=
Tff7b72when Tb4b4b4(Tff7b72thisTb4b4b4) Tb4b4b4{
Tff7b72is Te6edf3RadioNotConnectedException Tff7b72-Tff7b72> Tff7b72thisTb4b4b4.Te6edf3message Tff7b72?: Ta5d6ff"Ta5d6ffDevice not foundTa5d6ff"
Tff7b72is Te6edf3NoSuchElementExceptionTb4b4b4,
Tff7b72is Te6edf3IllegalArgumentExceptionTb4b4b4,
Tff7b72-Tff7b72> Ta5d6ff"Ta5d6ffRequired characteristic missingTa5d6ff"
Tff7b72else Tff7b72-Tff7b72> Tff7b72thisTb4b4b4.Te6edf3message Tff7b72?: Tff7b72thisTff7b72::Te6edf3classTb4b4b4.Te6edf3simpleName Tff7b72?: Ta5d6ff"Ta5d6ffUnknownTa5d6ff"
Tb4b4b4}
Tff7b72return Tff7b72false Te6edf3to Te6edf3msg
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.11s